package org.litepal.crud;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.litepal.crud.model.AssociationInfo;
import org.litepal.exceptions.DataSupportException;
import org.litepal.util.BaseUtility;
import org.litepal.util.Const;
import org.litepal.util.DBUtility;
import org.litepal.util.DSUtility;
import org.litepal.util.LogUtil;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;

/**
 * This is a component under DataSupport. It deals with the saving stuff as
 * primary task. All the implementation based on the java reflection API and
 * Android SQLiteDatabase API. It will persist the model class into table. If
 * there're other associated models with the passed in class, the associated
 * models will be persisted too.
 * 
 * @author Tony Green
 * @since 1.1
 */
class SaveHandler extends DataHandler {

	/**
	 * Instance of SQLiteDatabase, use to do the saving job.
	 */
	private SQLiteDatabase mDatabase;

	/**
	 * The open interface for other classes in CRUD package to save a model. It
	 * will assign db to global variable {@link SaveHandler#mDatabase} for
	 * saving use. Then it will call {@link SaveHandler#onSave(DataSupport)}
	 * where the real saving business happens to do the saving job.
	 * 
	 * @param baseObj
	 *            The DataSupport object to persist.
	 * @param db
	 *            The instance of SQLiteDatabase.
	 */
	void onSave(DataSupport baseObj, SQLiteDatabase db) {
		mDatabase = db;
		onSave(baseObj);
	}

	/**
	 * It is called when a model class calls the save method, First of all the
	 * passed in baseObj will be saved into database. Then LitePal will analyze
	 * the foreign key name and value. If there's a associated model detected,
	 * cascading mode will be used to save all the associated models.
	 * 
	 * @param baseObj
	 *            Base object to persist.
	 */
	private void onSave(DataSupport baseObj) {
		List<Field> supportedFields = getSupportedFields(baseObj.getClass().getName());
		if (supportedFields != null) {
			try {
				analyzeRelatedModels(baseObj);
				if (!baseObj.isSaved()) {
					doSaveAction(baseObj, supportedFields);
				} else {
					doUpdateRelationAction(baseObj);
					LogUtil.d(TAG, "do update action. jump out here");
				}
			} catch (SecurityException e) {
				e.printStackTrace();
				throw new DataSupportException(DataSupportException.SECURITY_EXCEPTION);
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
				throw new DataSupportException(DataSupportException.ILLEGAL_ARGUMENT_EXCEPTION);
			} catch (IllegalAccessException e) {
				e.printStackTrace();
				throw new DataSupportException(DataSupportException.ILLEGAL_ACCESS_EXCEPTION);
			} catch (InvocationTargetException e) {
				e.printStackTrace();
				throw new DataSupportException(DataSupportException.INVOCATION_TARGET_EXCEPTION);
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
				throw new DataSupportException(DataSupportException.NO_SUCH_METHOD_EXCEPTION);
			} catch (NoSuchFieldException e) {
				e.printStackTrace();
				throw new DataSupportException(DataSupportException.NO_SUCH_FIELD_EXCEPTION);
			}
		}
	}

	/**
	 * Persisting model class into database happens here. The ultimate way to
	 * saving data is still using
	 * {@link SQLiteDatabase#insert(String, String, ContentValues)}. But putting
	 * the values for ContentValues is delegated to
	 * {@link SaveHandler#putContentValues(Field, Class, ContentValues)}. Note
	 * that SaveSupport won't help with id. Any developer who wants to set value
	 * to id will be ignored here. The id value will be generated by SQLite
	 * automatically.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param supportedFields
	 *            List of all supported fields.
	 * @param foreignKeyName
	 *            Foreign key column name.
	 * @param foreignKeyValue
	 *            Foreign key column value.
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws IllegalArgumentException
	 * @throws InvocationTargetException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 * @throws NoSuchFieldException
	 */
	private void doSaveAction(DataSupport baseObj, List<Field> supportedFields)
			throws SecurityException, IllegalArgumentException, NoSuchMethodException,
			IllegalAccessException, InvocationTargetException, NoSuchFieldException {
		String idName = null;
		Class<?> idType = null;
		ContentValues values = new ContentValues();
		for (Field field : supportedFields) {
			if (isIdColumn(field.getName())) {
				idName = field.getName();
				idType = field.getType();
			} else {
				putContentValues(baseObj, field, values);
			}
		}
		putForeignKeyValue(values, baseObj);
		long id = mDatabase.insert(getTableName(baseObj), null, values);
		assignIdValue(baseObj, idName, idType, id);
		saveAssociatedModels(baseObj);
	}

	/**
	 * When a model is associated with two different models.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 */
	private void doUpdateRelationAction(DataSupport baseObj) throws SecurityException,
			IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
			InvocationTargetException {
		for (DataSupport associatedModel : baseObj.getAssociatedModels()) {
			if (hasForeignKeyValue(baseObj, associatedModel)) {
				ContentValues values = new ContentValues();
				values.put(getForeignKeyName(associatedModel.getClass().getName()),
						associatedModel.getBaseObjId());
				mDatabase.update(getTableName(baseObj), values, "id = ?",
						new String[] { String.valueOf(baseObj.getBaseObjId()) });
			}
		}
	}

	/**
	 * Assign the generated id value to the model. The
	 * {@link DataSupport#baseObjId} will be assigned anyway. If the model has a
	 * field named id or _id, LitePal will assign it too. The
	 * {@link DataSupport#baseObjId} will be used as identify of this model for
	 * system use. The id or _id field will help developers for their own
	 * purpose.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param idName
	 *            The name of id. Only id or _id is valid.
	 * @param idType
	 *            The type of id. Only int or long is valid.
	 * @param id
	 *            The value of id.
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 */
	private void assignIdValue(DataSupport baseObj, String idName, Class<?> idType, long id)
			throws SecurityException, IllegalArgumentException, NoSuchFieldException,
			IllegalAccessException {
		giveBaseObjIdValue(baseObj, id);
		giveModelIdValue(baseObj, idName, idType, id);
	}

	/**
	 * Assign the generated id value to {@link DataSupport#baseObjId}. This
	 * value will be used as identify of this model for system use.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param id
	 *            The value of id.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	private void giveBaseObjIdValue(DataSupport baseObj, long id) throws SecurityException,
			NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
		if (id > 0) {
			Field baseObjIdField = DataSupport.class.getDeclaredField("baseObjId");
			baseObjIdField.setAccessible(true);
			baseObjIdField.set(baseObj, id);
		}
	}

	/**
	 * After saving a model, the id for this model will be returned. Assign this
	 * id to the model's id or _id field if it exists.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param idName
	 *            The name of id. Only id or _id is valid.
	 * @param idType
	 *            The type of id. Only int or long is valid.
	 * @param id
	 *            The value of id.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	private void giveModelIdValue(DataSupport baseObj, String idName, Class<?> idType, long id)
			throws SecurityException, NoSuchFieldException, IllegalArgumentException,
			IllegalAccessException {
		if (shouldGiveModelIdValue(idName, idType, id)) {
			Field idField = baseObj.getClass().getDeclaredField(idName);
			idField.setAccessible(true);
			if (idField.getType() == int.class || idField.getType() == Integer.class) {
				idField.set(baseObj, (int) id);
			} else if (idField.getType() == long.class || idField.getType() == Long.class) {
				idField.set(baseObj, id);
			} else {
				throw new DataSupportException(DataSupportException.ID_TYPE_INVALID_EXCEPTION);
			}
		}
	}

	/**
	 * Find all the associated models of currently model. Then add all the
	 * associated models into {@link DataSupport#associatedModels} of baseObj.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param foreignKeyId
	 *            The id value of foreign key.
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws NoSuchMethodException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	private void analyzeRelatedModels(DataSupport baseObj) throws SecurityException,
			IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
			InvocationTargetException {
		List<String> classNames = new ArrayList<String>();
		classNames.add(getBaseClassName(baseObj));
		Collection<AssociationInfo> associatedInfos = getAssociationInfo(baseObj.getClass()
				.getName());
		for (AssociationInfo associationInfo : associatedInfos) {
			if (associationInfo.getAssociationType() == Const.Model.MANY_TO_ONE) {
				new Many2OneAnalyzer().analyze(baseObj, associationInfo);
			} else if (associationInfo.getAssociationType() == Const.Model.ONE_TO_ONE) {
				new One2OneAnalyzer().analyze(baseObj, associationInfo);
			} else if (associationInfo.getAssociationType() == Const.Model.MANY_TO_MANY) {
				new Many2ManyAnalyzer().analyze(baseObj, associationInfo);
			}
		}
		LogUtil.d(TAG, "self class is " + getBaseClassName(baseObj));
		LogUtil.d(TAG, "associated models are " + baseObj.getAssociatedModels());
	}

	/**
	 * Calling {@link #onSave(DataSupport, String, long)} method by recursion.
	 * Saving each associated model.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param field
	 *            The field which associated with other model.
	 * @param associatedField
	 *            The field on the associated model's side which associated with
	 *            the original model.
	 * @param foreignKeyName
	 *            The name of foreign key.
	 * @param foreignKeyId
	 *            The value for foreign key id.
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws NoSuchMethodException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	private void saveAssociatedModels(DataSupport baseObj) throws SecurityException,
			IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
			InvocationTargetException {
		// DataSupport[] associatedModels =
		// getAssociatedModels(baseObj).toArray(new DataSupport[0]);
		// for (DataSupport associatedModel : associatedModels) {
		// onSave(associatedModel);
		// }
		DataSupport[] associatedModels = baseObj.getAssociatedModels().toArray(new DataSupport[0]);
		for (DataSupport associatedModel : associatedModels) {
			onSave(associatedModel);
		}
	}

	/**
	 * If this model has associated models, then need to persist those models
	 * too. This method gets the foreign key name in tables. The case of
	 * appended _id will be changed as rules.
	 * 
	 * @param className
	 *            The class name of base object class.
	 * @return The foreign key name in tables.
	 */
	private String getForeignKeyName(String className) {
		return BaseUtility.changeCase(getForeignKeyColumnName(DBUtility
				.getTableNameByClassName(className)));
	}

	/**
	 * This method deals with the putting values job into ContentValues. The
	 * ContentValues has <b>put</b> method to set data. But we do not know we
	 * should use which <b>put</b> method cause the field type isn't clear. So
	 * the reflection API is necessary here to put values into ContentValues
	 * with dynamically getting field type to put value.
	 * 
	 * @param baseObj
	 *            The class of base object.
	 * @param field
	 *            Field to put into ContentValues.
	 * @param values
	 *            The instance of ContentValues.
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	private void putContentValues(DataSupport baseObj, Field field, ContentValues values)
			throws SecurityException, IllegalArgumentException, NoSuchMethodException,
			IllegalAccessException, InvocationTargetException {
		Object[] parameters = new Object[] { BaseUtility.changeCase(field.getName()),
				takeGetMethodValueByField(baseObj, field) };
		Class<?>[] parameterTypes;
		if (field.getType().isPrimitive()) {
			parameterTypes = new Class[] { String.class, DSUtility.getObjectType(field.getType()) };
		} else {
			parameterTypes = new Class[] { String.class, field.getType() };
		}
		DynamicExecutor.send(values, "put", parameters, values.getClass(), parameterTypes);
	}

	/**
	 * If the table for this model have a foreign key column, the value of
	 * foreign key id should be saved too.
	 * 
	 * @param values
	 *            The instance of ContentValues to put foreign key value.
	 * @param foreignKeyName
	 *            Foreign key column name.
	 * @param foreignKeyValue
	 *            Foreign key column value.
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 */
	private void putForeignKeyValue(ContentValues values, DataSupport baseObj)
			throws SecurityException, IllegalArgumentException, NoSuchMethodException,
			IllegalAccessException, InvocationTargetException {
		for (DataSupport associatedModel : baseObj.getAssociatedModels()) {
			if (hasForeignKeyValue(baseObj, associatedModel)) {
				values.put(getForeignKeyName(associatedModel.getClass().getName()),
						associatedModel.getBaseObjId());
			}
		}
	}

	/**
	 * Judge should assign id value to model's id field. The principle is that
	 * if id name is not null, id type is not null and id is greater than 0,
	 * then should assign id value to it.
	 * 
	 * @param idName
	 *            The name of id field.
	 * @param idType
	 *            The type of id field.
	 * @param id
	 *            The value of id.
	 * @return If id name is not null, id type is not null and id is greater
	 *         than 0, return true. Otherwise return false.
	 */
	private boolean shouldGiveModelIdValue(String idName, Class<?> idType, long id) {
		return idName != null && idType != null && id > 0;
	}

	/**
	 * Judge if this model has foreign key value or not. Calling
	 * {@link DataHandler#hasForeignKeyColumn(DataSupport, DataSupport)} to
	 * judge if the baseObj has a foreign key column of the associated model.
	 * Then calling {@link DataSupport#getBaseObjId()} of associated model to
	 * see if the value returned is greater than 0. If both of the results are
	 * true, return true.
	 * 
	 * @param baseObj
	 *            The base object model.
	 * @param associatedModel
	 *            The associated model of baseObj.
	 * @return Return true if baseObj has foreign key value, otherwise return
	 *         false.
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 */
	private boolean hasForeignKeyValue(DataSupport baseObj, DataSupport associatedModel)
			throws SecurityException, IllegalArgumentException, NoSuchMethodException,
			IllegalAccessException, InvocationTargetException {
		return hasForeignKeyColumn(baseObj, associatedModel) && associatedModel.isSaved();
	}

	/**
	 * Get the table name of baseObj.
	 * 
	 * @param baseObj
	 *            The base object model.
	 * @return The table name of baseObj.
	 */
	private String getTableName(DataSupport baseObj) {
		return DBUtility.getTableNameByClassName(baseObj.getClass().getName());
	}

}
